package algorithm.graph.adjacencymatrix;

import algorithm.graph.base.Edge;
import algorithm.graph.base.GraphEvent;

import java.util.*;
import java.util.function.BiConsumer;

public class AdjacencyMatrixGraphEdgeTraverse {
    AdjacencyMatrixGraph graph;

    public AdjacencyMatrixGraphEdgeTraverse(AdjacencyMatrixGraph graph) {
        this.graph = graph;
    }

    public static class RuntimeContext {
        boolean[] visitedNode;
        boolean[] visitingNode;
        Map<String, Boolean> visitedEdge;
        Map<String, Boolean> visitingEdge;

        public RuntimeContext(int nodeCount) {
            visitedNode = new boolean[nodeCount];
            Arrays.fill(visitedNode, false);
            visitingNode = new boolean[nodeCount];
            Arrays.fill(visitingNode, false);
            visitedEdge = new HashMap<>();
            visitingEdge = new HashMap<>();
        }

        public boolean isNodeNotVisited(Integer index) {
            return !visitingNode[index] && !visitedNode[index];
        }

        public void setNodeVisited(Integer index) {
            visitedNode[index] = true;
            visitingNode[index] = false;
        }

        public boolean isEdgeNotVisited(Integer from, Integer to) {
            String key = getKey(from, to);
            visitingEdge.computeIfAbsent(key, it -> false);
            visitedEdge.computeIfAbsent(key, it -> false);
            return !visitingEdge.get(key) && !visitedEdge.get(key);
        }

        private static String getKey(Integer from, Integer to) {
            String key = String.format("%d->%d", Math.min(from, to), Math.max(from, to));
            return key;
        }

        public void setNodeVisiting(Integer index) {
            visitedNode[index] = true;
        }

        public void setEdgeVisited(int from, int to) {
            String key = getKey(from, to);
            visitedEdge.put(key, true);
            visitingEdge.put(key, false);
        }

        public void setEdgeVisiting(int from, int to) {
            String key = getKey(from, to);
            visitingEdge.put(key, true);
        }
    }

    public void traverseEdge(BiConsumer<int[][], Edge> onVisitEdge) {
        Stack<GraphEvent> eventStack = new Stack<GraphEvent>();
        eventStack.push(new GraphEvent("init", this));
        int nodeCount = graph.getNodeCount();
        int[][] graphData = graph.getGraphData();
        RuntimeContext runtimeContext = new RuntimeContext(nodeCount);
        while (!eventStack.empty()) {
            GraphEvent event = eventStack.pop();
            String eventType = event.getEventType();
            switch (eventType) {
                case "init" -> {
                    if (graphData != null) {
                        eventStack.push(new GraphEvent("visitGraph", null));
                    }
                }
                case "visitGraph" -> {
                    for (int i = nodeCount - 1; i >= 0; i--) {
                        eventStack.push(new GraphEvent("visitNode", i));
                    }
                }
                case "visitNode" -> {
                    Integer index = (Integer) event.getData();
                    if (runtimeContext.isNodeNotVisited(index)) {
                        int[] edges = graphData[index];
                        for (int i = nodeCount - 1; i >= 0; i--) {
                            if (edges[i] != 0) {
                                Edge edge = new Edge(index, i, edges[i]);
                                eventStack.push(new GraphEvent("visitEdge", edge));
                            }
                        }
                        eventStack.push(new GraphEvent("postVisitNode", index));
                        eventStack.push(new GraphEvent("onVisitNode", index));
                        eventStack.push(new GraphEvent("preVisitNode", index));
                    }
                }
                case "postVisitNode" -> {
                    Integer index = (Integer) event.getData();
                    runtimeContext.setNodeVisited(index);
                }
                case "onVisitNode" -> {
                    Integer index = (Integer) event.getData();
//                    onVisitNode.accept(graph, index);
                }
                case "preVisitNode" -> {
                    Integer index = (Integer) event.getData();
                    runtimeContext.setNodeVisiting(index);
                }
                case "postVisitEdge" -> {
                    Edge edge = (Edge) event.getData();
                    runtimeContext.setEdgeVisited(edge.from(), edge.to());
                }
                case "visitEdge" -> {
                    Edge edge = (Edge) event.getData();
                    if (runtimeContext.isEdgeNotVisited(edge.from(), edge.to())) {
                        eventStack.push(new GraphEvent("visitNode", edge.to()));
                        eventStack.push(new GraphEvent("postVisitEdge", edge));
                        eventStack.push(new GraphEvent("onVisitEdge", edge));
                        eventStack.push(new GraphEvent("preVisitEdge", edge));
                    }
                }
                case "onVisitEdge" -> {
                    Edge edge = (Edge) event.getData();
                    onVisitEdge.accept(graphData, edge);
                }
                case "preVisitEdge" -> {
                    Edge edge = (Edge) event.getData();
                    runtimeContext.setEdgeVisiting(edge.from(), edge.to());
                }
            }
        }
    }
}
